#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Formetto is a fast web widget framework with a very convenient interface.

Formetto uses the javascript files formetto.js and strDic.js,
which must be referenced by any page using this framework. This provides
behavior for required fields and validation on the client.

Currently Formetto does not offer any help for server-side validation,
but this may change.

http://code.google.com/p/webpyte/wiki/FormettoFramework
Author: Nando Florestan
License: LGPL
"""

# http://docs.python.org/whatsnew/pep-328.html
from __future__ import absolute_import
from __future__ import print_function   # deletes the print statement

import os, logging
from itertools import chain
from cherrypy import log, request, response
from xml.sax import saxutils # http://www.xml.com/pub/a/2002/11/13/py-xml.html
from StringIO import StringIO

try:  # # Use simplejson or Python 2.6 json, prefer simplejson.
    from simplejson.encoder import encode_basestring
except ImportError: # http://docs.python.org/library/json.html
    from       json.encoder import encode_basestring

try:
    from genshi import Markup
except ImportError, e: # Not using Genshi? No problem.
    Markup = unicode
    log.error("Formetto unable to: from genshi import Markup",
              severity=logging.WARNING)



class BaseWidget(object):
    """Base class for widgets."""
    def __init__(self, id=None, value=None, form=None):
        self.innertext = None
        self.form = form    # a Formetto instance
        self.id = id
        
        if value is None and form is not None:
            self.value = form.get_value(id)
        else:
            self.value = value
        
        if form is not None and form.prefix_ and id is not None:
            self.mangled_id = self.form.prefix_ + id
        else:
            self.mangled_id = id
        
        # The *attribs* list contains the XHTML tag attributes:
        self.attribs = []
        self.setattrib("id", self.mangled_id) # name of the field
    
    def setattrib(self, name, value):
        if value == None: return
        self.attribs.append((name, value))
    
    def get_js(self):
        return ""
    
    def _render_tag(self):
        s = StringIO()
        write = s.write
        write('<')
        write(self.tag)
        for (key, val) in self.attribs:
            if val is None: continue
            write(" ")
            write(key)
            write('=')
            write(saxutils.quoteattr(unicode(val)))
        if self.innertext is None:
            write(" />")
        else:
            write('>')
            write(self.innertext)
            write("</")
            write(self.tag)
            write(">")
        if hasattr(self, "_aftertag"):
            self._aftertag(write)
        write("\n")
        return s.getvalue()
    
    def _before_render(self):
        pass
    
    def __str__(self):
        self._before_render()
        return Markup(self._render_tag())



class Hidden(BaseWidget):
    """Represents a hidden input."""
    tag = "input"

    def __init__(self, **kw):
        super(Hidden, self).__init__(**kw)
        self.setattrib("name", self.mangled_id)
        self.setattrib("type", "hidden")
        self.setattrib("value", self.value)



class DisplayWidget(BaseWidget):
    """Base class for all widgets that appear for the user to see."""
    def __init__(self, help=None, title=None, label=None, css=None,
                 style=None, **kw):
        """The text in the *help* parameter is added after the widget and
        usually appears to the right of it.
        
        The *title* parameter creates a tooltip.
        
        If the *label* parameter is provided, an entire table row is
        created, containing 2 columns: a label and the widget itself.
        """
        super(DisplayWidget, self).__init__(**kw)
        self.help = help
        self.label = label
        self.setattrib("title", title)
        self.setattrib("class", css)
        self.setattrib('style', style)
    
    def _aftertag(self, write):
        if self.help is not None:
            write(" ")
            write(self.help)
        if hasattr(self, "_afterhelp"):
            self._afterhelp(write)
    
    def __str__(self):
        self._before_render()
        tag = self._render_tag()
        if self.label:
            return Markup(self._form_row(tag))
        else:
            return Markup(tag)
    
    def _form_row(self, widget):
        return u"""
<tr>
  <th><label for="%s">%s:</label></th>
  <td>%s</td>
</tr>""" % (self.mangled_id, saxutils.escape(self.label), widget)



class Display(DisplayWidget):
    """Represents a <div> that displays some text."""
    tag = "div"
    
    def __init__(self, css="FormettoDisplay", sizeable=True,
                 height=None, **kw):
        super(Display, self).__init__(css=css, **kw)
        self.sizeable = sizeable
        if height: self.setattrib("style", "height:%dpx;" % height)
    
    def _before_render(self):
        if self.value is None:
            self.innertext = ""
        else:
            self.innertext = \
                saxutils.escape(self.value).replace("\n", "<br />\n")
    
    handle = property(fget = lambda self: self.mangled_id + "_handle")
    
    def _form_row(self, widget):
        """The div is vertically sizeable unless sizeable = False."""
        if self.sizeable == 0:
            return Markup("""
<tr>
  <th>%s:</th>
  <td>%s</td>
</tr>""" % (saxutils.escape(self.label), widget))
        elif self.sizeable == 2:
            row = u"""
<tr>
  <th>%s:<br /><a class="ReduceArea" title="reduzir" onclick="reduceArea('%s');">-</a>&nbsp;<a name="ExpandArea" title="expandir" onclick="expandArea('%s');">+</a></th>
  <td>%s</td>
</tr>""" % (saxutils.escape(self.label), self.mangled_id, self.mangled_id, widget)
        else: # sizeable == 1:
            row = u"""
<tr>
  <th>%s:<br /><span id="%s" class="ResizeHandle" title="arraste p/dimensionar">resize</span></th>
  <td>%s</td>
</tr>""" % (saxutils.escape(self.label), self.handle, widget)
        return Markup(row)
    
    def get_js(self):
        if not self.sizeable or self.sizeable == 2:  return ""
        s = StringIO()
        write = s.write
        write('\nvar wid = q("')
        write(self.mangled_id)
        write('");\n')
        write('wid.makeResizable({handle:"')
        write(self.handle)
        write('", limit: {x: [40, 800], y: [20, 800]}});\n')
        return Markup(s.getvalue())



class Submit(DisplayWidget):
    tag = 'input'
    
    def __init__(self, css=None, form=None, action=None, validate=True,
                 confirmation=None, **kw):
        if css is None:
            css = 'button' if form is None or not form.using_wymeditor else \
                  'button wymupdate'
        super(Submit, self).__init__(css=css, form=form, **kw)
        self.action = action
        self.validate = validate
        self.confirmation = confirmation
    
    def _before_render(self):
        self.setattrib('type', 'submit')
        self.setattrib('value', self.value)
    
    def get_js(self):
        s = StringIO()
        write = s.write
        write('\nvar wid = q("')
        write(self.mangled_id)
        write('");\n')
        write('wid.onclick = function(e) {\n')
        if self.action:
            write('  this.form.action = "')
            write(self.action)
            write('";\n')
        if self.validate:
            funcName = "validateConfirmSend"
        else:
            funcName = "confirmSend"
        write("  return ")
        write(funcName)
        write("(this")
        if self.confirmation:
            write(', ')
            write(encode_basestring(self.confirmation))
        write(");\n}")
        return Markup(s.getvalue())



class Button(Submit):
    """You should usually prefer *Submit* over *Button*. Here is why.
    
    The *button* tag is supported in all major browsers. Inside a button
    element you can put HTML (text, images etc.). This is the difference
    between this element and submit buttons created with the *input* element.
    
    The *type* attribute should always be specified. The default button
    type for Internet Explorer is "button", while in other browsers (and the
    W3C specification) it is "submit". Only the "submit" type automatically
    submits the form. In Formetto the default type is "submit".
    
    Be careful when using the button element in a form. In a form the
    different browsers will submit different values. IE (even 7) will
    submit the text between the <button> and </button> tags, while other
    browsers will send the content of the value attribute. When in a form,
    use the input element instead.
    
    In addition to sending the wrong text, IE6 always sends all buttons, though
    only one of them was pressed! One cannot determine which button was pressed
    (must use script for that).
    
    In order to maintain a uniform interface between Button and Submit classes,
    Formetto treats the *value* parameter as what appears on the page
    (for Button, the HTML between the <button> and </button> tags).
    If you really must set the "value" attribute, use the *val* parameter.
    """
    tag = "button"
    
    def __init__(self, type='submit', val=None, **kw):
        super(Button, self).__init__(**kw)
        self.setattrib('value', val)
        if type in ('submit', 'button', 'reset'):
            self._type = type
        else:
            raise RuntimeError('A button cannot have this type: ' + type)
    
    def _before_render(self):
        self.setattrib('type', self._type)
        self.innertext = self.value
    


class EditWidget(DisplayWidget):
    """Base class for all widgets whose value can be edited by the user."""
    def __init__(self, required=None, onchange=None, **kw):
        super(EditWidget, self).__init__(**kw)
        self.setattrib("name", self.mangled_id)
        self.setattrib("onchange", onchange)
        if required is None and self.form is not None: # autodetect required
            required = self.form.is_required(self.id)
        self.required = required # whether this field is mandatory
    
    REQUIRED = "&nbsp;*" # Text that gets written after mandatory widgets
    
    def _afterhelp(self, write):
        if self.required:  write(self.REQUIRED)
    
    def _more_js(self, write):
        pass
    
    def get_js(self):
        s = StringIO()
        write = s.write
        write('\nvar wid = q("')
        write(self.mangled_id)
        write('");\n')
        if self.required:  write('wid.required = true;\n')
        if self.label:
            write('wid.label = ')
            write(encode_basestring(unicode(self.label)))
            write(";\n")
        self._more_js(write) # subclasses can add js code
        return s.getvalue()
    


class Text(EditWidget):
    """Represents an HTML text input."""
    tag = "input"
    size_limit = 80

    def __init__(self, css="textfield", size=None, password=False,
                 maxlength=None, onkeypress=None, **kw):
        """The *maxlength* parameter sets the max number of characters
        in this textbox.
        """
        super(Text, self).__init__(css=css, **kw)
        if password:
            self.setattrib("type", "password")
        else:
            self.setattrib("type", "text")
        if maxlength is None and self.form is not None:
            maxlength = self.form.get_length(self.id)
        if size is None and maxlength is not None:
            if maxlength > self.size_limit:
                size = self.size_limit
            else:
                size = maxlength
        self.setattrib("maxlength", maxlength)
        self.setattrib("size", size) # cols for this textbox
        self.setattrib("onkeypress", onkeypress)
        self._set_val(self.value, maxlength)
    
    def _set_val(self, value, maxlength):
        '''Sets the 'value' attribute. Useful to override this method.
        '''
        self.setattrib("value", value)
    


class DateTime(Text):
    '''Represents a text input that should contain date and time.'''
    # TODO: Add javascript validation for date
    
    def __init__(self, maxlength=None, **kw):
        if maxlength is None: maxlength = 16
        super(DateTime, self).__init__(maxlength=maxlength, **kw)
    
    def _set_val(self, value, maxlength):
        if value is not None:  value = str(value)[:maxlength]
        self.setattrib('value', value)



class SimpleTextArea(EditWidget):
    """Represents an HTML textarea widget."""
    tag = "textarea"
    
    def __init__(self, css="textfield", rows=4, cols=60, **kw):
        super(SimpleTextArea, self).__init__(css=css, **kw)
        self.setattrib("rows", rows)
        self.setattrib("cols", cols)
    
    def _before_render(self):
        if self.value is None:
            self.innertext = ""
        else:
            self.innertext = saxutils.escape(self.value) \
                             .replace("\r\n","\n").replace("\n","\r\n")



class TextArea(SimpleTextArea):
    '''A resizeable textarea widget (using jQuery).
    http://dev.jquery.com/view/tags/ui/latest/demos/functional/#ui.resizable
    '''
    
    def __init__(self, sizeable=True, **kw):
        super(TextArea, self).__init__(**kw)
        self.sizeable = sizeable
        # Import the necessary JS
        if sizeable and self.form is not None:
            self.form.scripts.include_jquery_ui()
            #self.form.scripts.include('/wp/jquery/jquery.ui.resizable.js')
    
    def _more_js(self, write):
        if not self.sizeable:  return
        write('$("#%s").resizable({ handles: "s,e,se" });' % self.id)



class Wymeditor(SimpleTextArea):
    '''Represents a rich text editor: http://www.wymeditor.org'''
    def __init__(self, lang=None, stylesheet=None, sizeable=True, **kw):
        super(Wymeditor, self).__init__(**kw)
        # Import the necessary JS
        if self.form is not None:
            self.form.scripts.include_wymeditor()
            self.form.using_wymeditor = True
        # Prepare the options
        self.opts = []
        self._add_opt('lang', lang)
        self._add_opt('stylesheet', stylesheet)
        # (Could also set minHeight and maxHeight.)
        # Sizeable option requires a js import and calling a function
        if sizeable:
            self.form.scripts.include('/wp/wymeditor/plugins/resizable/' \
                                      'jquery.wymeditor.resizable.js')
            self._add_opt('postInit', '''function(wym) {
    wym.resizable({ handles: 's,e,se' });
}''', as_string=False)
        # Activate, passing the options
        self.form.scripts.include_body("""jQuery(function() {
    jQuery('#%s').wymeditor({%s});
});""" % (self.id, ', '.join(self.opts)))
    
    def _add_opt(self, name, value, as_string=True):
        if value is not None:
            val = value if not as_string else "'%s'" % value
            self.opts.append("%s: %s" % (name, val))


class Select(EditWidget):
    """Represents an HTML select widget."""
    tag = "select"
    
    def __init__(self, options, css="singleselectfield", multiple=None,
                 size=None, topoption=None, **kw):
        """The 'options' parameter is the list of choices in the select field.
        It should be a list of tuples in the form (value, content, selected).
        For example:
            options = [(1, "Shlomo Mintz", True), (2, "Midori", False)]

        If 'multiple' is True, a multiple selection box is created,
        in which case 'size' determines its number of rows.
        """
        super(Select, self).__init__(css=css, **kw)
        self.setattrib("size", size)
        if multiple:  self.setattrib("multiple", "multiple")
        self.options = options
        if topoption: self.options.insert(0, topoption)
    
    def _before_render(self):
        s = StringIO()
        write = s.write
        write("\n")
        for (value, text, selected) in self.options:
            write('  <option value=')
            write(saxutils.quoteattr(unicode(value)))
            if selected:  write(' selected="selected"')
            write('>')
            write(saxutils.escape(unicode(text)))
            write('</option>\n')
        self.innertext = s.getvalue()



class Checkbox(EditWidget):
    tag = "input"

    def __init__(self, css="checkbox", **kw):
        super(Checkbox, self).__init__(css=css, **kw)
        self.setattrib("type", "checkbox")
        if self.value: self.setattrib("checked", "checked")



class File(EditWidget):
    """Represents a file upload widget.
    In order to upload, the web form must have this attribute:
        enctype="multipart/form-data"
    """
    tag = "input"

    def __init__(self, css="fileupload", size=None, **kw):
        super(File, self).__init__(css=css, **kw)
        self.setattrib("type", "file")
        self.setattrib("size", size)



class ScriptsManager(object):
    '''Keeps track of the javascript imports that a page requires,
    in order to avoid importing them more than once.
    Can also be used for javascript bodies which are needed only once.
    
    After including everything the page needs, retrieve the result by
    calling this object. A Genshi example:
    
        ${request.scripts_manager(focusfirst=True)}
    '''
    def __init__(self):
        self._includes = ['/wp/formetto.js', '/wp/strDic.js']
        self._bodies = []
        self.formettos = [] # Formetto instances managed by this object
    
    def include(self, script):
        if script not in self._includes:
            self._includes.append(script)
    
    def include_jquery(self):
        self.include("/wp/jquery/jquery.js")
    
    def include_jquery_ui(self):
        self.include_jquery()
        #self.include('/wp/jquery/jquery.ui.all.packed.js')
        self.include('/wp/jquery/jquery.ui.all.js')
    
    def include_wymeditor(self):
        self.include_jquery()
        self.include("/wp/wymeditor/jquery.wymeditor.pack.js")
    
    def include_body(self, body):
        self._bodies.append(body)
    
    def get_includes(self):
        return ['<script type="text/ecmascript" src="{0}"></script>' \
                .format(s) for s in self._includes]
    
    def __call__(self, focusfirst=False, more_script=''):
        """Returns the javascript imports necessary for this page,
        as well as javascript bodies required by its forms.
        *focusfirst* is False by default. If you pass a form name, when the
        page loads, the first widget in that form gets the focus. If you
        just pass True, the first widget in the first form gets the focus.
        You may also pass *more_script* at this time.
        """
        wids = chain(*(f.get_js() for f in self.formettos))
        if focusfirst:
            if isinstance(focusfirst, basestring):
                foc = '\ninputz.focusFirst("%s");' % focusfirst
            else:
                foc = '\ninputz.focusFirst();'
        else:
            foc = ''
        m = '\n'.join(chain(self.get_includes(),
                            ('<script type="text/ecmascript">',),
                            self._bodies,
                            wids,
                            (foc,
                             more_script,
                             '</script>\n')))
        return Markup(m)
            



class Formetto(object):
    """A Formetto instance provides services for building an HTML form,
    such as keeping track of all the controls, populating their values,
    setting their maxlength, aggregating the necessary javascript etc.
    """
    using_wymeditor = False
    
    def __init__(self, obj=None, prefix=None):
        """The parameter *obj* is the object being edited in this web form
        (source of its shown values). If it is a SQLAlchemy ORM object,
        it can also provide the maxlength.
        
        *prefix* - if passed - is appended to the beginning of the widget id,
        plus an underscore. This is useful to create a namespace in the form.
        This is almost a necessity when you want to edit 2 or more objects in
        a single web form.
        """
        self.widgets = []
        self.obj = obj
        self.prefix  = prefix
        self.prefix_ = prefix + "_" if prefix else None
        # There is only one ScriptsManager per request.
        if not hasattr(request, 'scripts_manager'):
            request.scripts_manager = ScriptsManager()
        # In addition to the request object, reference it in this Formetto
        self.scripts = request.scripts_manager
        # The ScriptsManager also has to know about this Formetto
        self.scripts.formettos.append(self)
    
    def obj():
        def fget(self):
            return self._obj
        def fset(self, val):
            self._obj = val
            # Find the _cols only once:
            if val is None:
                self._cols = None
            else:
                cm = val._sa_class_manager
                if hasattr(cm, "mapper"):
                    self._cols = cm.mapper.columns
        return locals()
    obj = property(**obj())
    
    def get_col(self, name):
        try:
            return self._cols[name]
        except KeyError:
            return self._cols.get("_" + name, None)
        except TypeError:
            return None
    
    def __repr__(self):
        if self.prefix:
            p = "'" + self.prefix + "'"
        else:
            p = "None"
        return "".join([
            "Formetto(obj=",
            repr(self.obj),
            ", prefix=",
            p,
            ")",
        ])
    
    @staticmethod
    def separate(dic, prefix):
        """When a form with namespaces (prefixed widgets) is submitted, you
        need a way to split the submitted dictionary by prefix. Pass the
        dictionary and the prefix to this method; it will pop the prefixed
        entries and return them as a new dictionary. Its keys come without
        the prefix.
        """
        out = {}
        prefix_ = prefix + "_"
        for key in dic.keys():
            if key.startswith(prefix_):
                k = key[len(prefix_):]
                out[k] = dic.pop(key)
        return out
    
    def get_js(self):
        """Returns a generator that yields the javascript code for each of
        the widgets in this form.
        """
        return (w.get_js() for w in self.widgets)

    def get_value(self, field):
        """Returns the value for the given field,
        provided this Formetto instance was initialized with an object.
        """
        if self.obj and not isinstance(self.obj, type) and field:
            try:
                val = getattr(self.obj, field)
            except AttributeError:
                return None
            if val == u"":  return None # so no HTML attribute is written
            return val
        else:
            return None
    
    def get_length(self, field):
        if not self.obj:  return None
        t = self.get_col(field)
        if t is None:
            return None
        t = t.type
        if hasattr(t, "length"):
            return t.length
        else:
            return None
    
    def get_default(self, field):
        """Returns the default value of *field* as defined in the
        SQLAlchemy mapping class.
        """
        if not self.obj:  return None
        return self.get_col(field).default.arg
    
    def is_required(self, field):
        """Returns True if the specified field is required
        (not nullable).
        """
        c = self.get_col(field)
        if c is None:
            return None
        else:
            return not c.nullable
    
    def _encapsulate(cls):
        def func(self, *a, **kw):
            widget = cls(form=self, *a, **kw)
            self.widgets.append(widget)
            return widget
        return func

    # Formetto instances can instantiate widgets (passing itself to them)
    # while adding them to its widgets list. Here are methods to do that:
    Text     = _encapsulate(Text)
    DateTime = _encapsulate(DateTime)
    TextArea = _encapsulate(TextArea)
    Checkbox = _encapsulate(Checkbox)
    Hidden   = _encapsulate(Hidden)
    File     = _encapsulate(File)
    Button   = _encapsulate(Button)
    Submit   = _encapsulate(Submit)
    Select   = _encapsulate(Select)
    Display  = _encapsulate(Display)
    Wymeditor = _encapsulate(Wymeditor)
    SimpleTextArea = _encapsulate(SimpleTextArea)
    # In this case the method names are pascal-case on purpose,
    # so they are identical to the corresponding classes.
